package com.nc.mem.serial.graph;

import static com.nc.mem.serial.graph.Representation.readInt;
import static com.nc.mem.serial.graph.Representation.writeClassId;
import static com.nc.mem.serial.graph.Representation.writeInt;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel.MapMode;

import org.slf4j.LoggerFactory;

import com.nc.mem.serial.Serializer;
import com.nc.mem.serial.graph.asm.GraphSerializerFactory;


/**
 * General class for serializing a graph of objects. Usually one will get instances of this class
 * via the GraphSerializerFactoryl
 * 
 * @author cmuramoto
 */
public abstract class GraphSerializer implements Serializer {

	public static void serialize(final Object o, final File target, final int expectedSize) {
		final Serializer serializer = GraphSerializerFactory.serializer(o.getClass());

		RandomAccessFile raf = null;
		MappedByteBuffer map = null;

		try {
			raf = new RandomAccessFile(target, "rw");
			map = raf.getChannel().map(MapMode.READ_WRITE, 0, expectedSize);
			serializer.write(map, o);
			raf.setLength(map.position());
		} catch (final Exception e) {
			target.delete();
			throw new RuntimeException(e);
		} finally {
			if (raf != null) {
				try {
					raf.close();
				} catch (final IOException ex) {
					LoggerFactory.getLogger(GraphSerializer.class).error("Error closing " + target.getPath(), ex);
				}
			}
			unmap(map);
		}
	}

	private static void unmap(final MappedByteBuffer map) {
		if (map != null && map.isDirect()) {

		}
	}

	public abstract void doRead(ByteBuffer src, Object obj);

	public abstract void doWrite(ByteBuffer dest, Object obj);

	public abstract Object instantiate(ByteBuffer src);

	/**
	 * This method should be used only to read a root object. It will read and discard another int
	 * (the classId), since we are already now which serializer we are working on! <br/>
	 * <br/>
	 * This method compensates only for {@link GraphSerializer#write(ByteBuffer, Object)} when
	 * dealing with the root object, otherwise it might corrupt the stream! To read nested (maybe
	 * polymorphic) references, one should use either {@link GraphSerializer#read(ByteBuffer, int)}
	 * or {@link GraphSerializer#readNonRoot(ByteBuffer)}.
	 */
	@Override
	public final Object read(final ByteBuffer src) {
		final int s = readInt(src);
		readInt(src);
		return read(src, s);
	}

	@Override
	public final Object read(final ByteBuffer src, final int s) {
		if (s == 1) {
			return null;
		}
		final Context current = Context.current();

		if ((s & 1 << 1) != 0) {
			return current.fromId(s);
		}
		current.start();
		try {
			final Object o = instantiate(src);
			current.registerId(o, s);
			doRead(src, o);
			return o;
		} finally {
			current.clear();
		}
	}

	/**
	 * Reads an object if not already present on the context. Compensates
	 * {@link GraphSerializer#writeNonRoot(ByteBuffer, Object)}. Usually it's to be used when a
	 * Serializer calls other serializer.
	 * 
	 * @param dest
	 * @param o
	 */
	public final Object readNonRoot(final ByteBuffer src) {
		return read(src, readInt(src));
	}

	/**
	 * Writes the root of an object graph or a nested field. This method will also write the classId
	 * of the object if not already registered on the Context to cope with polymorphic references. <br/>
	 * <br/>
	 * This method should be compensated byte {@link GraphSerializer#read(ByteBuffer)} for the root
	 * object only! Otherwise it should be compensated by
	 * {@link GraphSerializer#read(ByteBuffer, int)}.
	 */
	@Override
	public final void write(final ByteBuffer dest, final Object root) {
		final Context context = Context.current().start();
		final int s = context.registerObject(root);
		try {
			writeInt(dest, s);
			if ((s & (1 | 1 << 1)) == 0) {// not null and not a ptr
				writeClassId(dest, root);
				doWrite(dest, root);
			}
		} finally {
			context.clear();
		}
	}

	/**
	 * Writes an object, but don't write it's class. This method is intended to be used on a
	 * serializer that calls other serializer and is to be compensated by
	 * {@link GraphSerializer#readNonRoot(ByteBuffer)}.
	 * 
	 * @param dest
	 * @param o
	 */
	public final void writeNonRoot(final ByteBuffer dest, final Object o) {
		final int s = Context.current().registerObject(o);
		writeInt(dest, s);
		if ((s & (1 | 1 << 1)) == 0) {
			doWrite(dest, o);
		}
	}

}
